package com.randy.recyclerviews.adapter;

import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.LongSparseArray;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;

import com.randy.recyclerviews.R;
import com.randy.recyclerviews.adapter.eventhook.EventHook;
import com.randy.recyclerviews.adapter.eventhook.EventHookHelper;
import com.randy.recyclerviews.adapter.eventhook.OnClickEventHook;
import com.randy.recyclerviews.adapter.eventhook.OnLongClickEventHook;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static android.view.View.NO_ID;

/**
 * @author xudong
 * @since 2017/2/6
 */

public class UniversalAdapter extends RecyclerView.Adapter<UniversalAdapter.ViewHolder> {
    private static final String LOG_TAG = UniversalAdapter.class.getSimpleName();
    private static final String SAVED_STATE_ARG_VIEW_HOLDERS = "saved_state_view_holders";

    private final ModelList models = new ModelList();

    private final EventHookHelper eventHookHelper = new EventHookHelper(this);
    private boolean isAttached = false;

    private final LongSparseArray<ViewHolder> boundViewHolders = new LongSparseArray<>();
    private ViewHolderState viewHolderState = new ViewHolderState();

    //<editor-fold desc="GridLayout support">
    private final GridLayoutManager.SpanSizeLookup spanSizeLookup = new GridLayoutManager
            .SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            AbstractModel<?> model = getModel(position);
            return model != null ? model.getSpanSize(spanCount, position, getItemCount()) : 1;
        }
    };

    private int spanCount = 1;

    public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() {
        return spanSizeLookup;
    }

    public void setSpanCount(int spanCount) {
        this.spanCount = spanCount;
    }
    //</editor-fold>

    public UniversalAdapter() {
        setHasStableIds(true);
        spanSizeLookup.setSpanIndexCacheEnabled(true);
    }

    //<editor-fold desc="CRUD Method">
    @Nullable
    public AbstractModel<?> getModel(int position) {
        return position >= 0 && position < models.size() ? models.get(position) : null;
    }

    @Deprecated
    public List<AbstractModel<?>> getModels() {
        //todo: not allow get this model list directly
        return models;
    }

    public boolean containsModel(AbstractModel<?> model) {
        return models.indexOf(model) >= 0;
    }

    protected int indexOfModel(AbstractModel<?> model) {
        return models.indexOf(model);
    }

    @NonNull
    protected List<AbstractModel<?>> getAllModelSubListAfter(@Nullable AbstractModel<?> model) {
        int index = models.indexOf(model);
        if (index == -1) return Collections.emptyList();
        return models.subList(index + 1, models.size());
    }

    @NonNull
    public List<AbstractModel<?>> getAllModelListAfter(@Nullable AbstractModel<?> model) {
        int index = models.indexOf(model);
        if (index == -1) return Collections.emptyList();

        return new ArrayList<>(models.subList(index + 1, models.size()));
    }

    @NonNull
    public List<AbstractModel<?>> getAllModelListBetween(@Nullable AbstractModel<?> start,
                                                         @Nullable AbstractModel<?> end) {
        int startIdx = models.indexOf(start),
                endIdx = models.indexOf(end);
        startIdx = startIdx == -1 ? 0 : startIdx + 1;
        endIdx = endIdx == -1 ? models.size() : endIdx;
        if (startIdx > endIdx) return Collections.emptyList();

        return new ArrayList<>(models.subList(startIdx, endIdx));
    }

    public void addModel(@NonNull AbstractModel<?> modelToAdd) {
        final int initialSize = models.size();

        models.add(modelToAdd);
        notifyItemInserted(initialSize);
    }

    public void addModel(int index, @NonNull AbstractModel<?> modelToAdd) {
        if (index > models.size() || index < 0) return;

        models.add(index, modelToAdd);
        notifyItemInserted(index);
    }

    public void addModels(@NonNull AbstractModel<?>... modelsToAdd) {
        addModels(Arrays.asList(modelsToAdd));
    }

    public void addModels(@NonNull Collection<? extends AbstractModel<?>> modelsToAdd) {
        final int initialSize = models.size();

        models.addAll(modelsToAdd);
        notifyItemRangeInserted(initialSize, modelsToAdd.size());
    }

    public void insertModelBefore(@NonNull AbstractModel<?> modelToInsert,
                                  @Nullable AbstractModel<?> modelToInsertBefore) {
        int targetIndex = models.indexOf(modelToInsertBefore);
        if (targetIndex == -1) return;

        models.add(targetIndex, modelToInsert);
        notifyItemInserted(targetIndex);
    }

    public void insertModelsBefore(@NonNull Collection<? extends AbstractModel<?>> modelsToInsert,
                                   @Nullable AbstractModel<?> modelToInsertBefore) {
        int targetIndex = models.indexOf(modelToInsertBefore);
        if (targetIndex == -1) return;

        models.addAll(targetIndex, modelsToInsert);
        notifyItemRangeInserted(targetIndex, modelsToInsert.size());
    }

    public void insertModelAfter(@NonNull AbstractModel<?> modelToInsert,
                                 @Nullable AbstractModel<?> modelToInsertAfter) {
        int modelIndex = models.indexOf(modelToInsertAfter);
        if (modelIndex == -1) return;

        int targetIndex = modelIndex + 1;

        models.add(targetIndex, modelToInsert);
        notifyItemInserted(targetIndex);
    }

    public void insertModelsAfter(@NonNull Collection<? extends AbstractModel<?>> modelsToInsert,
                                  @Nullable AbstractModel<?> modelToInsertAfter) {
        int modelIndex = models.indexOf(modelToInsertAfter);
        if (modelIndex == -1) return;

        int targetIndex = modelIndex + 1;

        models.addAll(targetIndex, modelsToInsert);
        notifyItemRangeInserted(targetIndex, modelsToInsert.size());
    }

    public void notifyModelChanged(@NonNull AbstractModel<?> model) {
        notifyModelChanged(model, null);
    }

    public void notifyModelChanged(@NonNull AbstractModel<?> model, @Nullable Object payload) {
        int index = models.indexOf(model);
        if (index != -1) {
            notifyItemChanged(index, payload);
        }
    }

    public void removeModel(@Nullable AbstractModel<?> modelToRemove) {
        int index = models.indexOf(modelToRemove);
        if (index >= 0 && index < models.size()) {
            models.remove(index);

            notifyItemRemoved(index);
        }
    }

    public void removeAllModels() {
        final int initialSize = models.size();

        models.clear();
        notifyItemRangeRemoved(0, initialSize);
    }

    public void removeAllAfterModel(@Nullable AbstractModel<?> model) {
        final int initialSize = models.size();

        List<AbstractModel<?>> modelsToRemove = getAllModelSubListAfter(model);
        int numModelsRemoved = modelsToRemove.size();
        if (numModelsRemoved == 0) return;

        //clear the sublist
        modelsToRemove.clear();
        notifyItemRangeRemoved(initialSize - numModelsRemoved, numModelsRemoved);
    }

    public void replaceAllModels(@NonNull final List<? extends AbstractModel<?>> modelsToReplace) {
        if (models.size() == 0) {
            addModels(modelsToReplace);
        } else {
            models.clear();
            notifyDataSetChanged();
            addModels(modelsToReplace);
        }
    }
    //</editor-fold>

    //<editor-fold desc="Core">
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = models.viewHolderFactory.create(viewType,
                LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false));

        eventHookHelper.bind(viewHolder);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@Nullable ViewHolder holder, int position) {
        onBindViewHolder(holder, position, Collections.emptyList());
    }

    @Override
    public void onBindViewHolder(@Nullable ViewHolder holder, int position,
                                 @Nullable List<Object> payloads) {
        final AbstractModel model = getModel(position);
        if (holder == null || model == null) return;

        // A ViewHolder can be bound again even it is already bound and showing, like when it is on
        // screen and is changed. In this case we need
        // to carry the state of the previous view over to the new view. This may not be necessary if
        // the viewholder is reused (see RecyclerView.ItemAnimator#canReuseUpdatedViewHolder)
        // but we don't rely on that to be safe and to simplify
        // ??????????
        if (boundViewHolders.get(holder.getItemId()) != null) {
            viewHolderState.save(boundViewHolders.get(holder.getItemId()));
        }

        holder.bind(model, payloads);

        viewHolderState.restore(holder);
        boundViewHolders.put(holder.getItemId(), holder);
    }

    @Override
    public void onViewRecycled(@Nullable ViewHolder holder) {
        if (holder == null) return;

        viewHolderState.save(holder);
        boundViewHolders.remove(holder.getItemId());

        holder.unbind();
    }

    @Override
    public void onViewAttachedToWindow(ViewHolder holder) {
        final AbstractModel model = holder.model;
        if (model == null) return;
        //noinspection unchecked,rawtypes
        model.attachedToWindow(holder);
    }

    @Override
    public void onViewDetachedFromWindow(ViewHolder holder) {
        final AbstractModel model = holder.model;
        if (model == null) return;
        //noinspection unchecked,rawtypes
        model.detachedFromWindow(holder);
    }

    @Override
    public int getItemCount() {
        return models.size();
    }

    @Override
    public int getItemViewType(int position) {
        AbstractModel model = getModel(position);
        return model == null ? NO_ID : model.getLayoutRes();
    }

    @Override
    public long getItemId(int position) {
        AbstractModel model = getModel(position);
        return model == null ? NO_ID : model.id();
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        isAttached = true;
    }

    public void onSaveInstanceState(Bundle outState) {
        for (int i = 0; i < boundViewHolders.size(); i++) {
            long key = boundViewHolders.keyAt(i);
            viewHolderState.save(boundViewHolders.get(key));
        }

        if (viewHolderState.size() > 0 && !hasStableIds()) {
            throw new IllegalStateException("Must have stable ids when saving view holder state");
        }

        outState.putParcelable(SAVED_STATE_ARG_VIEW_HOLDERS, viewHolderState);
    }

    public void onRestoreInstanceState(@Nullable Bundle inState) {
        // To simplify things we enforce that state is restored before views are bound, otherwise it
        // is more difficult to update view state once they are bound
        if (boundViewHolders.size() > 0) {
            throw new IllegalStateException(
                    "State cannot be restored once views have been bound. It should be done before adding "
                            + "the adapter to the recycler view.");
        }

        if (inState != null) {
            ViewHolderState savedState = inState.getParcelable(SAVED_STATE_ARG_VIEW_HOLDERS);
            if (savedState != null) {
                viewHolderState = savedState;
            } else {
                Log.w(LOG_TAG, "can not get save viewholder state");
            }
        }
    }
    //</editor-fold>

    //<editor-fold desc="Model">
    public static class ViewHolder extends RecyclerView.ViewHolder {
        @Nullable
        private AbstractModel model;

        public ViewHolder(View itemView) {
            super(itemView);
        }

        private void bind(@NonNull AbstractModel model, @Nullable List<Object> payloads) {
            if (payloads != null && !payloads.isEmpty()) {
                // noinspection unchecked
                model.bindData(this, payloads);
            } else {
                // noinspection unchecked
                model.bindData(this);
            }

            this.model = model;
        }

        private void unbind() {
            if (model == null) return;
            // noinspection unchecked
            model.unbind(this);
            model = null;
        }

        boolean shouldSaveViewState() {
            return model != null && model.shouldSaveViewState();
        }
    }

    private static class ViewHolderFactory {
        private final SparseArray<IViewHolderCreator<?>> creatorSparseArray = new SparseArray<>();

        void register(@NonNull AbstractModel<?> model) {
            if (creatorSparseArray.get(model.getLayoutRes()) == null) {
                creatorSparseArray.put(model.getLayoutRes(), model.getViewHolderCreator());
            }
        }

        void register(@NonNull Collection<? extends AbstractModel<?>> models) {
            for (final AbstractModel model : models) {
                if (model == null) continue;
                if (creatorSparseArray.get(model.getLayoutRes()) == null) {
                    creatorSparseArray.put(model.getLayoutRes(), model.getViewHolderCreator());
                }
            }
        }

        ViewHolder create(@LayoutRes int viewType, @NonNull View view) {
            IViewHolderCreator<?> viewHolderCreator = creatorSparseArray.get(viewType);
            if (viewHolderCreator == null) {
                throw new RuntimeException("cannot find viewHolderCreator for viewType="
                        + viewType);
            }
            return viewHolderCreator.create(view);
        }
    }

    private static class ModelList extends ArrayList<AbstractModel<?>> {
        private final ViewHolderFactory viewHolderFactory = new ViewHolderFactory();

        @Override
        public boolean add(@NonNull AbstractModel<?> model) {
            viewHolderFactory.register(model);
            return super.add(model);
        }

        @Override
        public void add(int index, @NonNull AbstractModel<?> element) {
            viewHolderFactory.register(element);
            super.add(index, element);
        }

        @Override
        public boolean addAll(@NonNull Collection<? extends AbstractModel<?>> c) {
            viewHolderFactory.register(c);
            return super.addAll(c);
        }

        @Override
        public boolean addAll(int index, @NonNull Collection<? extends AbstractModel<?>> c) {
            viewHolderFactory.register(c);
            return super.addAll(index, c);
        }
    }

    /**
     * The fundamental unit of the adapter. It provides the view layout, the viewHolder creator,
     * the binding rules from data to viewHolder, and the diff logic.
     * <p>
     * Each model should have its own identity, defaultValue {@link #id}
     * otherwise may cause the {@link DiffUtil#calculateDiff(DiffUtil.Callback)} failed,
     * even "Inconsistency detected" exception
     */
    public static abstract class AbstractModel<T extends ViewHolder>
            implements IDiffUtilHelper<AbstractModel<?>> {
        private static long idCounter = NO_ID - 1;
        /**
         * used to unique identify a model in the RecyclerView, see {@link #getItemId(int)}
         * <p>
         * can be written by {@link #AbstractModel(long)} or override by {@link #id()}
         * TODO: not allow change once set
         */
        private long id;

        protected AbstractModel(long id) {
            this.id = id;
        }

        public AbstractModel() {
            this(idCounter--);
        }

        /**
         * model's own identity.
         * use {@link #id(long)} and similar methods to customize
         */
        public final long id() {
            return id;
        }

        /**
         * get the number of spans occupied by this model in the adapter
         * <p>
         * only used for {@link GridLayoutManager}
         *
         * @param totalSpanCount max span count the model can occupied
         * @param position       position in the {@link #models}
         * @param itemCount      total model count
         */
        public int getSpanSize(int totalSpanCount, int position, int itemCount) {
            return 1;
        }

        /**
         * Whether the adapter should save the state of the view bound to this model.
         */
        public boolean shouldSaveViewState() {
            return false;
        }

        @LayoutRes
        public abstract int getLayoutRes();

        /**
         * Binds the data to the viewHolder.
         */
        public void bindData(@NonNull T holder) {
        }

        /**
         * Binds the data to the viewHolder with payloads.
         *
         * @param payloads A non-null list of merged payloads. Can be empty list if requires full
         *                 update.
         * @see RecyclerView.Adapter#notifyItemChanged(int, Object)
         */
        public void bindData(@NonNull T holder, @Nullable List<Object> payloads) {
            bindData(holder);
        }

        /**
         * Subclasses can override this if their view needs to release resources when it's recycled.
         */
        public void unbind(@NonNull T holder) {
        }

        /**
         * Called when the itemView has been attached to a window.
         * <p>
         * <p>This can be used as a reasonable signal that the view is about to be seen
         * by the user. If the adapter previously freed any resources in
         * {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow}
         * those resources should be restored here.</p>
         *
         * @param holder Holder of the view being attached
         */
        public void attachedToWindow(@NonNull T holder) {
        }

        /**
         * Called when the itemView has been detached from its window.
         * <p>
         * <p>Becoming detached from the window is not necessarily a permanent condition;
         * the consumer of an Adapter's views may choose to cache views offscreen while they
         * are not visible, attaching and detaching them as appropriate.</p>
         *
         * @param holder Holder of the view being detached
         */
        public void detachedFromWindow(@NonNull T holder) {
        }

        /**
         * Used to create viewHolder corresponding to the {@link #getLayoutRes()}.
         */
        @NonNull
        public abstract IViewHolderCreator<T> getViewHolderCreator();

        /**
         * Called to compare this model with another model, normally just compare the {@link #id()}.
         *
         * @param item another model to be compared.
         *             can be safely cast to the same class of this model
         * @return True if the two models represent the same object or false if they are different.
         * @see android.support.v7.util.DiffUtil.Callback#areItemsTheSame(int, int)
         */
        @Override
        public boolean isItemTheSame(@NonNull AbstractModel<?> item) {
            return id() == item.id();
        }

        /**
         * Called to compare this model with another model.
         *
         * @param item another model to be compared.
         *             can be safely cast to the same class of this model
         * @return True if the contents of the models are the same or false if they are different.
         * @see android.support.v7.util.DiffUtil.Callback#areContentsTheSame(int, int)
         */
        @Override
        public boolean isContentTheSame(@NonNull AbstractModel<?> item) {
            return true;
        }

        protected void id(long id) {
            if (id == -1) return;
            this.id = id;
        }

        /**
         * Use multiple numbers as the id for this model.
         *
         * @param ids not allow null child, if found, use old id
         */
        protected void id(Number... ids) {
            long result = 0;
            for (Number id : ids) {
                if (id == null) return;
                result = 31 * result + hashLong64Bit(id.hashCode());
            }
            id(result);
        }

        /**
         * Use two numbers as model id.
         */
        protected void id(long id1, long id2) {
            long result = hashLong64Bit(id1);
            result = 31 * result + hashLong64Bit(id2);
            id(result);
        }

        /**
         * Use a string as the model id.
         *
         * @param key not allow null
         */
        protected void id(@Nullable CharSequence key) {
            if (key == null) return;

            id(hashString64Bit(key));
        }

        /**
         * Use several strings to define the id of the model.
         *
         * @param key       not allow null
         * @param otherKeys not allow null child, if found, use old id
         */
        protected void id(@Nullable CharSequence key, CharSequence... otherKeys) {
            if (key == null) return;

            long result = hashString64Bit(key);
            for (CharSequence otherKey : otherKeys) {
                if (otherKey == null) return;
                result = 31 * result + hashString64Bit(otherKey);
            }
            id(result);
        }

        /**
         * Use a string and a number as model id.
         *
         * @param key not allow null, if found, use old id
         */
        protected void id(@Nullable CharSequence key, long id) {
            if (key == null) return;

            long result = hashString64Bit(key);
            result = 31 * result + hashLong64Bit(id);
            id(result);
        }

        /**
         * Hash a long into 64 bits instead of the normal 32. This uses a xor shift implementation to
         * attempt psuedo randomness so object ids have an even spread for less chance of collisions.
         * <p>
         * From http://stackoverflow.com/a/11554034
         * <p>
         * http://www.javamex.com/tutorials/random_numbers/xorshift.shtml
         */
        private static long hashLong64Bit(long value) {
            value ^= (value << 21);
            value ^= (value >>> 35);
            value ^= (value << 4);
            return value;
        }

        /**
         * Hash a string into 64 bits instead of the normal 32. This allows us to better use strings as a
         * model id with less chance of collisions. This uses the FNV-1a algorithm for a good mix of speed
         * and distribution.
         * <p>
         * Performance comparisons found at http://stackoverflow.com/a/1660613
         * <p>
         * Hash implementation from http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-1a
         */
        private static long hashString64Bit(@NonNull CharSequence str) {
            long result = 0xcbf29ce484222325L;
            final int len = str.length();
            for (int i = 0; i < len; i++) {
                result ^= str.charAt(i);
                result *= 0x100000001b3L;
            }
            return result;
        }
    }

    /**
     * Interface definition for a callback to be invoked when creating a viewHolder
     */
    public interface IViewHolderCreator<VH extends ViewHolder> {
        /**
         * @param view inflated view of {@link AbstractModel#getLayoutRes()}
         */
        @NonNull
        VH create(@NonNull View view);
    }
    //</editor-fold>

    //<editor-fold desc="Wrapper Model">
    public static abstract class WrapperViewHolder<VH extends ViewHolder> extends ViewHolder {
        @NonNull
        protected final VH childViewHolder;

        @NonNull
        public VH getChildViewHolder() {
            return childViewHolder;
        }

        public WrapperViewHolder(View itemView, @NonNull VH childViewHolder) {
            super(itemView);
            this.childViewHolder = childViewHolder;
        }

        @Override
        public boolean shouldSaveViewState() {
            return childViewHolder.shouldSaveViewState();
        }
    }

    public static abstract class AbstractWrapperModel<VH extends WrapperViewHolder<MVH>,
            M extends AbstractModel<MVH>, MVH extends ViewHolder> extends AbstractModel<VH> {
        //todo: not allow attached model as childModel
        @NonNull
        protected final M childModel;

        @NonNull
        public M getChildModel() {
            return childModel;
        }

        public AbstractWrapperModel(@NonNull M childModel) {
            super();
            this.childModel = childModel;
        }

        @Override
        public boolean shouldSaveViewState() {
            return childModel.shouldSaveViewState();
        }

        @CallSuper
        @Override
        public void bindData(@NonNull VH holder) {
            childModel.bindData(holder.getChildViewHolder());
        }

        @CallSuper
        @Override
        public void bindData(@NonNull VH holder, @Nullable List<Object> payloads) {
            childModel.bindData(holder.getChildViewHolder(), payloads);
        }

        @CallSuper
        @Override
        public void unbind(@NonNull VH holder) {
            childModel.unbind(holder.getChildViewHolder());
        }

        @CallSuper
        @Override
        public void attachedToWindow(@NonNull VH holder) {
            childModel.attachedToWindow(holder.getChildViewHolder());
        }

        @CallSuper
        @Override
        public void detachedFromWindow(@NonNull VH holder) {
            childModel.detachedFromWindow(holder.getChildViewHolder());
        }

        @NonNull
        @Override
        public abstract WrapperViewHolderCreator<VH, MVH> getViewHolderCreator();

        @Override
        public boolean isItemTheSame(@NonNull AbstractModel<?> item) {
            return super.isItemTheSame(item)
                    && childModel.isItemTheSame(((AbstractWrapperModel) item).childModel);
        }

        @Override
        public boolean isContentTheSame(@NonNull AbstractModel<?> item) {
            return super.isContentTheSame(item)
                    && childModel.isContentTheSame(((AbstractWrapperModel) item).childModel);
        }
    }

    public static abstract class WrapperViewHolderCreator<VH extends WrapperViewHolder<MVH>,
            MVH extends ViewHolder> implements IViewHolderCreator<VH> {
        @LayoutRes
        private final int childLayoutRes;
        @NonNull
        private final IViewHolderCreator<MVH> childViewHolderCreator;

        public WrapperViewHolderCreator(@LayoutRes int childLayoutRes,
                                        @NonNull IViewHolderCreator<MVH> childViewHolderCreator) {
            this.childLayoutRes = childLayoutRes;
            this.childViewHolderCreator = childViewHolderCreator;
        }

        @NonNull
        @Override
        public VH create(@NonNull View view) {
            ViewStub viewStub = (ViewStub) view.findViewById(R.id.view_model_child_stub);
            if (viewStub == null) {
                throw new IllegalStateException("layout must have ViewStub{id=view_model_child_stub}");
            }
            viewStub.setLayoutResource(childLayoutRes);
            return create(view, childViewHolderCreator.create(viewStub.inflate()));
        }

        public abstract VH create(@NonNull View view, MVH childViewHolder);
    }
    //</editor-fold>

    //<editor-fold desc="Event Hook">

    /**
     * Register an eventHook to {@link #models}
     *
     * @throws IllegalStateException this method MUST be called before the ViewHolder is created
     */
    public <VH extends ViewHolder> void addEventHook(
            @NonNull EventHook<VH> eventHook) {
        if (isAttached) {
            Log.w(LOG_TAG, "addEventHook is called after adapter attached");
        }
        // noinspection unchecked
        eventHookHelper.add(eventHook);
    }
    //</editor-fold>

    //<editor-fold desc="OnClickListener">
    @Nullable
    private OnItemClickListener onItemClickListener;
    @Nullable
    private EventHook<ViewHolder> onItemClickEventHook;

    private void addOnItemClickEventHook() {
        onItemClickEventHook = new OnClickEventHook<ViewHolder>(
                ViewHolder.class) {
            @Override
            public void onClick(@NonNull View view, @NonNull ViewHolder viewHolder,
                                int position, @NonNull AbstractModel rawModel) {
                if (onItemClickListener != null) {
                    onItemClickListener.onClick(view, viewHolder, position, rawModel);
                }
            }

            @Nullable
            @Override
            public View onBind(@NonNull ViewHolder viewHolder) {
                return viewHolder.itemView.isClickable() ? viewHolder.itemView : null;
            }
        };
        addEventHook(onItemClickEventHook);
    }

    /**
     * Register a callback to be invoked when {@link #models} are clicked.
     * If the view of this model is not clickable, it will not trigger callback.
     *
     * @throws IllegalStateException this method must be called before
     *                               {@link RecyclerView#setAdapter(RecyclerView.Adapter)}
     */
    public void setOnItemClickListener(@Nullable OnItemClickListener onItemClickListener) {
        if (isAttached && onItemClickEventHook == null && onItemClickListener != null) {
            throw new IllegalStateException("setOnItemClickListener " +
                    "must be called before the RecyclerView#setAdapter");
        } else if (!isAttached && onItemClickEventHook == null) {
            addOnItemClickEventHook();
        }
        this.onItemClickListener = onItemClickListener;
    }

    /**
     * Interface definition for a callback to be invoked when a model is clicked.
     */
    public interface OnItemClickListener {
        /**
         * Called when a model has been clicked.
         *
         * @param itemView   The view that was clicked.
         * @param viewHolder The viewHolder that was clicked.
         * @param position   Adapter position of the viewHolder,
         *                   see {@link RecyclerView.ViewHolder#getAdapterPosition()}.
         * @param model      The abstractModel that was clicked.
         */
        void onClick(@NonNull View itemView, @NonNull ViewHolder viewHolder,
                     int position, @NonNull AbstractModel<?> model);
    }
    //</editor-fold>

    //<editor-fold desc="OnLongClickListener">
    @Nullable
    private OnItemLongClickListener onItemLongClickListener;
    @Nullable
    private EventHook<ViewHolder> onItemLongClickEventHook;

    private void addOnItemLongClickEventHook() {
        onItemLongClickEventHook = new OnLongClickEventHook<ViewHolder>(ViewHolder.class) {
            @Override
            public boolean onLongClick(@NonNull View view, @NonNull ViewHolder viewHolder,
                                       int position, @NonNull AbstractModel rawModel) {
                return onItemLongClickListener != null && onItemLongClickListener.onLongClick(
                        view, viewHolder, position, rawModel);
            }

            @Nullable
            @Override
            public View onBind(@NonNull ViewHolder viewHolder) {
                return viewHolder.itemView.isClickable() ? viewHolder.itemView : null;
            }
        };
        addEventHook(onItemLongClickEventHook);
    }

    /**
     * Register a callback to be invoked when {@link #models} are clicked and held.
     * If the view of this model is not long clickable, it will not trigger callback.
     *
     * @throws IllegalStateException this method must be called before
     *                               {@link RecyclerView#setAdapter(RecyclerView.Adapter)}
     */
    public void setOnItemLongClickListener(@Nullable OnItemLongClickListener
                                                   onItemLongClickListener) {
        if (isAttached && onItemLongClickEventHook == null && onItemLongClickListener != null) {
            throw new IllegalStateException("setOnItemLongClickListener " +
                    "must be called before the RecyclerView#setAdapter");
        } else if (!isAttached && onItemLongClickEventHook == null) {
            addOnItemLongClickEventHook();
        }
        this.onItemLongClickListener = onItemLongClickListener;
    }

    /**
     * Interface definition for a callback to be invoked when a model has been clicked and held.
     */
    public interface OnItemLongClickListener {
        /**
         * Called when a model has been clicked and held.
         *
         * @param itemView   The view that was clicked and held.
         * @param viewHolder The viewHolder that was clicked and held.
         * @param position   Adapter position of the viewHolder,
         *                   see {@link RecyclerView.ViewHolder#getAdapterPosition()}.
         * @param model      The abstractModel that was clicked and held.
         * @return true if the callback consumed the long click, false otherwise.
         */
        boolean onLongClick(@NonNull View itemView, @NonNull ViewHolder viewHolder,
                            int position, @NonNull AbstractModel<?> model);
    }
    //</editor-fold>
}
